package com.example.lock.zk.impl;

import com.example.util.LockContext;
import com.example.util.ZkUtils;
import org.I0Itec.zkclient.IZkDataListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * @Description: ZK 解决羊群效应问题
 * 羊群效应：就是避免多个阻塞的JVM 同时唤醒，而只是唤醒其中的一个，基于 zk 临时顺序节点
 * @Author: mingtian
 * @CreateDate: 2020/12/7 14:39
 * @Version: 1.0
 */
@Component
public class ZkTemporaryOrderNodeLock extends AbstractLockTemplate {

    /**
     * 打印日志
     */
    private Logger logger = LoggerFactory.getLogger(ZkTemporaryOrderNodeLock.class);

    /**
     * 父节点
     */
    private String lockParent = "/lock";

    /**
     * 顺序节点
     */
    private String lockPath = "/lockPath";

    /**
     * 上一个临时顺序节点路径
     */
    private String prevPath;

    private CountDownLatch countDownLatch;

    @Override
    protected boolean tryLock() {
        // 1.创建临时顺序节点 先从 ThreadLocal 中获取 如果已经存在则直接获取 不需要再重新创建 nodeName,反之 需要创建 nodeName 并存放到 ThreadLocal 中
        String nodeName = LockContext.get();
        if (nodeName == null) {
            nodeName = ZkUtils.createZkClient().createEphemeralSequential(lockParent + lockPath, "lock");
            LockContext.set(nodeName);
        }
        // 2.获取父节点下面所有子节点  实现排序查找到最小节点
        List<String> children = ZkUtils.getZkClient().getChildren(lockParent);
        // 排序
        Collections.sort(children);
        // 3.如果当前节点 == 最小节点(第一个创建成功的)  获取锁成功
        if (nodeName.equals(lockParent + "/" + children.get(0))) {
            return true;
        }
        // 4.如果不是最小节点，则获取锁失败
        int index = Collections.binarySearch(children, nodeName.substring(lockParent.length() + 1));
        // 5.获取到上一个节点路径  订阅
        prevPath = lockParent + "/" + children.get(index - 1);
        // 6.当上一个节点释放锁时  开始唤醒重新获取锁
        return false;
    }

    @Override
    protected void waitLock() {
        IZkDataListener iZkDataListener = new IZkDataListener() {
            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {

            }

            @Override
            public void handleDataDeleted(String dataPath) throws Exception {
                logger.info("节点被删除,node:{}", dataPath);
                // 当节点被删除之后，唤醒阻塞的线程
                if (countDownLatch != null) {
                    countDownLatch.countDown();
                }
            }
        };
        // 事件监听
        ZkUtils.getZkClient().subscribeDataChanges(prevPath, iZkDataListener);
        try {
            // 如果已经存在节点的话 则进行阻塞
            if (ZkUtils.getZkClient().exists(prevPath)) {
                // 线程阻塞
                countDownLatch = new CountDownLatch(1);
                countDownLatch.await();
            }
        } catch (Exception e) {
        }
        // 当我们节点删除之后,我们应该重新被唤醒  移除监听事件
        ZkUtils.getZkClient().unsubscribeDataChanges(prevPath, iZkDataListener);
    }


}